<!--
@license
Copyright 2016 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../tf-imports/d3.html">

<!--
vz-histogram-timeseries creates an element that draws beautiful histograms for
displaying how data is distributed over time.

This histogram supports changing the time axis type and different modes of
visualization.

@element vz-histogram-timeseries
@demo demo/index.html
-->
<dom-module id="vz-histogram-timeseries">
    <template>
      <div id="tooltip"><span></span></div>
      <svg id="svg">
        <g>
          <g class="axis x"></g>
          <g class="axis y"></g>
          <g class="axis y slice"></g>
          <g class="stage">
            <rect class="background"></rect>
          </g>
          <g class="x-axis-hover"></g>
          <g class="y-axis-hover"></g>
          <g class="y-slice-axis-hover"></g>
        </g>
      </svg>

      <style>
        :host {
          display: flex;
          flex-direction: column;
          flex-grow: 1;
          flex-shrink: 1;
          position: relative;
        }

        svg {
          font-family: roboto, sans-serif;
          overflow: visible;
          display: block;
          width: 100%;
          flex-grow: 1;
          flex-shrink: 1;
        }

        #tooltip {
          position: absolute;
          display: block;
          opacity: 0;
          font-weight: bold;
          font-size: 11px;
        }

        .background {
          fill-opacity: 0;
          fill: red;
        }

        .histogram {
          pointer-events: none;
        }

        .hover {
          font-size: 9px;
          dominant-baseline: middle;
          opacity: 0;
        }

        .hover circle {
          stroke: white;
          stroke-opacity: 0.5;
          stroke-width: 1px;
        }

        .hover text {
          fill: black;
          opacity: 0;
        }

        .hover.hover-closest circle {
          fill: black!important;
        }

        .hover.hover-closest text {
          opacity: 1;
        }

        .baseline {
          stroke: black;
          stroke-opacity: 0.1;
        }

        .outline {
          fill: none;
          stroke: white;
          stroke-opacity: 0.5;
        }

        .outline.outline-hover {
          stroke: black!important;
          stroke-opacity: 1;
        }

        .x-axis-hover,
        .y-axis-hover,
        .y-slice-axis-hover {
          pointer-events: none;
        }

        .x-axis-hover .label,
        .y-axis-hover .label,
        .y-slice-axis-hover .label {
          opacity: 0;
          font-weight: bold;
          font-size: 11px;
          text-anchor: end;
        }

        .x-axis-hover text {
          text-anchor: middle;
        }

        .y-axis-hover text,
        .y-slice-axis-hover text {
          text-anchor: start;
        }

        .x-axis-hover line,
        .y-axis-hover line,
        .y-slice-axis-hover line {
          stroke: black;
        }

        .x-axis-hover rect,
        .y-axis-hover rect,
        .y-slice-axis-hover rect {
          fill: white;
        }

        .axis {
          font-size: 10px;
          fill: #aaa;
        }

        .axis path.domain {
          fill: none;
        }

        .axis .tick line {
          stroke: #ddd;
        }

        .axis.slice {
          opacity: 0;
        }

        .axis.slice .tick line {
          stroke-dasharray: 2;
        }

        .small .axis text { display: none; }
        .small .axis .tick:first-of-type text { display: block; }
        .small .axis .tick:last-of-type text { display: block; }
        .medium .axis text { display: none; }
        .medium .axis .tick:nth-child(2n + 1) text { display: block; }
        .large .axis text { display: none; }
        .large .axis .tick:nth-child(2n + 1) text { display: block; }

      </style>
    </template>

    <script>
    Polymer({
      is: "vz-histogram-timeseries",
      properties: {
        /**
         * Defines which view mode is being used by the chart. Supported values
         * are:
         * - "offset" - Offset view of the data showing all timesteps.
         * - "overlay" - Overlays all timesteps into one 2D view, with the
         * brighter lines representing the newer timesteps.
         */
        mode: {
          type: String,
          value: "offset"
        },

        /*
         * The name of the datum's property that contains the time values.
         * Allows:
         * - "step" - Linear scale using the "step" property of the datum.
         * - "wall_time" - Temporal scale using the "wall_time" property of the
         * datum.
         * - "relative" - Temporal scale starting at 0 created by using
         * the "wall_time" property of the datum.
         */
        timeProperty: {
          type: String,
          value: "step"
        },

        /**
         * The name of the data's property that contains the bins.
         */
        bins: {
          type: String,
          value: "bins"
        },

        /**
         * The name of the datum's property that contains the x values.
         */
        x: {
          type: String,
          value: "x"
        },

        /**
         * The name of the datum's property that contains the bin width values.
         */
        dx: {
          type: String,
          value: "dx"
        },

        /**
         * The name of the datum's property that contains the bin height.
         */
        y: {
          type: String,
          value: "y"
        },

        /**
         * Scale that maps series names to colors. The default colors are from
         * d3.scale.category10() scale. Use this property to replace the default
         * line colors with colors of your own choice.
         */
        colorScale: {
          type: Object,
          value: function() {
            return d3.scale.category10();
          }
        },

        /**
         * Duration of the transition between histogram modes.
         */
        modeTransitionDuration: {
          type: Number,
          value: 500
        },

        _attached: Boolean,
        _name: { type: String, value: null },
        _data: { type: Array, value: null },
      },
      observers: [
        'redraw(timeProperty, _attached)',
        '_modeRedraw(mode)'
      ],
      ready: function() {
        // Polymer's way of scoping styles on nodes that d3 created
        this.scopeSubtree(this.$.svg, true);
      },
      attached: function() {
        this._attached = true;
      },
      detached: function() {
        this._attached = false;
      },
      setVisibleSeries: function(names) {
        // Do nothing.
      },
      setSeriesData: function(name, data) {
        this._name = name;
        this._data = data;
        this.redraw();
      },

      /**
       * Redraws the chart. This is only called if the chart is attached to the
       * screen and if the chart has data.
       */
      redraw: function() {
        this._draw(0);
      },

      _modeRedraw: function() {
        this._draw(this.modeTransitionDuration);
      },

      _draw: function(duration) {
        if (!this._attached || !this._data) {
          return;
        }

        //
        // Data verification
        //
        if (duration === undefined) throw(new Error("vz-histogram-timeseries _draw needs duration"));
        if (this._data.length <= 0) throw(new Error("Not enough steps in the data"));
        if (!this._data[0].hasOwnProperty(this.bins)) throw(new Error("No bins property of '" + this.bins + "' in data"));
        if (this._data[0][this.bins].length <= 0) throw(new Error("Must have at least one bin in bins in data"));
        if (!this._data[0][this.bins][0].hasOwnProperty(this.x)) throw(new Error("No x property '" + this.x + "' on bins data"));
        if (!this._data[0][this.bins][0].hasOwnProperty(this.dx)) throw(new Error("No dx property '" + this.dx + "' on bins data"));
        if (!this._data[0][this.bins][0].hasOwnProperty(this.y)) throw(new Error("No y property '" + this.y + "' on bins data"));

        //
        // Initialization
        //
        var timeProp = this.timeProperty;
        var xProp = this.x;
        var binsProp = this.bins;
        var dxProp = this.dx;
        var yProp = this.y;

        var data = this._data;
        var name = this._name;
        var mode = this.mode;
        var color = d3.hcl(this.colorScale(name));
        var tooltip = d3.select(this.$.tooltip);

        var xAccessor = function(d) { return d[xProp] };
        var yAccessor = function(d) { return d[yProp] };
        var dxAccessor = function(d) { return d[dxProp] };
        var xRightAccessor = function(d) { return d[xProp] + d[dxProp] };
        var timeAccessor = function(d) { return d[timeProp] };

        if (timeProp === "relative") {
          timeAccessor = function(d) { return d.wall_time - data[0].wall_time };
        }

        var brect = this.$.svg.getBoundingClientRect();
        var outerWidth = brect.width,
            outerHeight = brect.height;

        var sliceHeight,
            margin = {top: 5, right: 60, bottom: 20, left: 24};

        if (mode === "offset") {
          sliceHeight = outerHeight / 2.5;
          margin.top = sliceHeight + 5;
        } else {
          sliceHeight = outerHeight - margin.top - margin.bottom;
        }

        var width = outerWidth - margin.left - margin.right,
            height = outerHeight - margin.top - margin.bottom;

        var leftMin = d3.min(data, xAccessor),
            rightMax = d3.max(data, xRightAccessor);

        //
        // Text formatters
        //
        var format = d3.format(".3n");
        var yAxisFormat = d3.format(".0f");

        if (timeProp === "wall_time") {
          yAxisFormat = d3.time.format("%m/%d %X");
        } else if (timeProp === "relative") {
          yAxisFormat = function(d) {
            return d3.format(".1r")(d / 3.6e6) + 'h'; // Convert to hours.
          };
        }

        //
        // Calculate the extents
        //
        var xExtents = data.map(function(d, i) {
          return [
            d3.min(d[binsProp], xAccessor),
            d3.max(d[binsProp], xRightAccessor)
          ];
        });
        var yExtents = data.map(function(d) {
          return d3.extent(d[binsProp], yAccessor);
        });

        //
        // Scales and axis
        //
        var outlineCanvasSize = 500;

        var extent = d3.extent(data, timeAccessor);

        var yScale = (timeProp === "wall_time" ? d3.time.scale() : d3.scale.linear())
            .domain(extent)
            .range([0, (mode === "offset" ? height : 0)]);

        var ySliceScale = d3.scale.linear()
            .domain([0, d3.max(data, function(d, i) { return yExtents[i][1]; })])
            .range([sliceHeight, 0]);

        var yLineScale = d3.scale.linear()
            .domain(ySliceScale.domain())
            .range([outlineCanvasSize, 0]);

        var xScale = d3.scale.linear()
            .domain([
              d3.min(data, function(d, i) { return xExtents[i][0]; }),
              d3.max(data, function(d, i) { return xExtents[i][1]; })
            ])
            .nice()
            .range([0, width]);

        var xLineScale = d3.scale.linear()
            .domain(xScale.domain())
            .range([0, outlineCanvasSize]);

        var outlineColor = d3.scale.linear()
            .domain(d3.extent(data, timeAccessor))
            .range([color.darker(), color.brighter()])
            .interpolate(d3.interpolateHcl);

        var xAxis = d3.svg.axis()
            .scale(xScale)
            .ticks(Math.max(2, width / 20))
            .orient("bottom");

        var yAxis = d3.svg.axis()
            .scale(yScale)
            .ticks(Math.max(2, height / 15))
            .tickFormat(yAxisFormat)
            .orient("right");

        var ySliceAxis = d3.svg.axis()
            .scale(ySliceScale)
            .ticks(Math.max(2, height / 15))
            .tickSize(width + 5)
            .tickFormat(format)
            .orient("right");

        var xBinCentroid = function(d) {
          return d[xProp] + d[dxProp] / 2;
        };

        var linePath = d3.svg.line()
            .interpolate("linear")
            .x(function(d) { return xLineScale(xBinCentroid(d)); })
            .y(function(d) { return yLineScale(d[yProp]); });

        var path = function(d) {
          // Draw a line from 0 to the first point and from the last point to 0.
          return 'M' + xLineScale(xBinCentroid(d[0])) + ',' + yLineScale(0) +
              'L' + linePath(d).slice(1) +
              "L" + xLineScale(xBinCentroid(d[d.length - 1])) + "," + yLineScale(0);
        };

        //
        // Render
        //
        var svgNode = this.$.svg;

        var svg = d3.select(svgNode)

        var svgTransition = svg.transition().duration(duration);

        var g = svg.select("g")
            .classed("small", function() { return (width > 0 && width <= 150); })
            .classed("medium", function() { return (width > 150 && width <= 300); })
            .classed("large", function() { return (width > 300); })

        var gTransition = svgTransition.select("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        var bisect = d3.bisector(xRightAccessor).left;
        var stage = g.select(".stage")
            .on("mouseover", function() {
              hoverUpdate.style("opacity", 1);
              xAxisHoverUpdate.style("opacity", 1);
              yAxisHoverUpdate.style("opacity", 1);
              ySliceAxisHoverUpdate.style("opacity", 1);
              tooltip.style("opacity", 1);
            })
            .on("mouseout", function() {
              hoverUpdate.style("opacity", 0);
              xAxisHoverUpdate.style("opacity", 0);
              yAxisHoverUpdate.style("opacity", 0);
              ySliceAxisHoverUpdate.style("opacity", 0);
              hoverUpdate.classed("hover-closest", false);
              outlineUpdate.classed("outline-hover", false);
              tooltip.style("opacity", 0);
            })
            .on("mousemove", onMouseMove);

        var background = stage.select(".background")
            .attr("transform", "translate(" + -margin.left + "," + -margin.top + ")")
            .attr("width", outerWidth)
            .attr("height", outerHeight);

        var histogram = stage.selectAll(".histogram").data(data),
            histogramExit = histogram.exit().remove(),
            histogramEnter = histogram.enter().append("g").attr("class", "histogram"),
            histogramUpdate = histogram
                .sort(function(a, b) { return timeAccessor(a) - timeAccessor(b); }),
            histogramTransition = gTransition.selectAll(".histogram")
                .attr("transform", function(d) {
                  return "translate(0, " +
                    (mode === "offset" ? (yScale(timeAccessor(d)) - sliceHeight) : 0) + ")";
                });

        var baselineEnter = histogramEnter.append("line").attr("class", "baseline"),
            baselineUpdate = histogramTransition.select(".baseline")
                .style("stroke-opacity", function(d) { return (mode === "offset" ? 0.1 : 0); })
                .attr("y1", sliceHeight)
                .attr("y2", sliceHeight)
                .attr("x2", width);

        var outlineEnter = histogramEnter.append("path").attr("class", "outline"),
            outlineUpdate = histogramUpdate.select(".outline")
                .attr("vector-effect", "non-scaling-stroke")
                .attr("d", function(d) { return path(d[binsProp]); })
                .style("stroke-width", 1),
            outlineTransition = histogramTransition.select(".outline")
                .attr("transform", "scale(" + width / outlineCanvasSize + ", " +
                      sliceHeight / outlineCanvasSize + ")")
                .style("stroke", function(d) {
                  return (mode === "offset" ? "white" : outlineColor(timeAccessor(d)));
                })
                .style("fill-opacity", function(d) { return (mode === "offset" ? 1 : 0); })
                .style("fill", function(d) { return outlineColor(timeAccessor(d)); });

        var hoverEnter = histogramEnter.append("g")
                .attr("class", "hover")
                .style("fill", function(d) { return outlineColor(timeAccessor(d)); }),
            hoverUpdate = histogramUpdate.select(".hover");

        hoverEnter.append("circle")
            .attr("r", 2);

        hoverEnter.append("text")
            .style("display", "none")
            .attr("dx", 4);

        var xAxisHover = g.select(".x-axis-hover").selectAll(".label").data(["x"]),
            xAxisHoverEnter = xAxisHover.enter().append("g").attr("class", "label"),
            xAxisHoverUpdate = xAxisHover;

        xAxisHoverEnter.append("rect")
            .attr("x", -20)
            .attr("y", 6)
            .attr("width", 40)
            .attr("height", 14)

        xAxisHoverEnter.append("line")
            .attr("x1", 0)
            .attr("x2", 0)
            .attr("y1", 0)
            .attr("y2", 6);

        xAxisHoverEnter.append("text")
            .attr("dy", 18);

        var yAxisHover = g.select(".y-axis-hover").selectAll(".label").data(["y"]),
            yAxisHoverEnter = yAxisHover.enter().append("g").attr("class", "label"),
            yAxisHoverUpdate = yAxisHover;

        yAxisHoverEnter.append("rect")
            .attr("x", 8)
            .attr("y", -6)
            .attr("width", 40)
            .attr("height", 14)

        yAxisHoverEnter.append("line")
            .attr("x1", 0)
            .attr("x2", 6)
            .attr("y1", 0)
            .attr("y2", 0);

        yAxisHoverEnter.append("text")
            .attr("dx", 8)
            .attr("dy", 4);

        var ySliceAxisHover = g.select(".y-slice-axis-hover").selectAll(".label").data(["y"]),
            ySliceAxisHoverEnter = ySliceAxisHover.enter().append("g").attr("class", "label"),
            ySliceAxisHoverUpdate = ySliceAxisHover;

        ySliceAxisHoverEnter.append("rect")
            .attr("x", 8)
            .attr("y", -6)
            .attr("width", 40)
            .attr("height", 14)

        ySliceAxisHoverEnter.append("line")
            .attr("x1", 0)
            .attr("x2", 6)
            .attr("y1", 0)
            .attr("y2", 0);

        ySliceAxisHoverEnter.append("text")
            .attr("dx", 8)
            .attr("dy", 4);

        gTransition.select(".y.axis.slice")
            .style("opacity", mode === "offset" ? 0 : 1)
            .attr("transform", "translate(0, " + (mode === "offset" ? -sliceHeight : 0) + ")")
            .call(ySliceAxis);

        gTransition.select(".x.axis")
            .attr("transform", "translate(0, " + height + ")")
            .call(xAxis);

        gTransition.select(".y.axis")
            .style("opacity", mode === "offset" ? 1 : 0)
            .attr("transform", "translate(" + width + ", " + (mode === "offset" ? 0 : height) + ")")
            .call(yAxis);

        function onMouseMove() {
          var m = d3.mouse(this),
              v = xScale.invert(m[0]),
              t = yScale.invert(m[1]);

          function hoverXIndex(d) {
            return Math.min(d[binsProp].length - 1, bisect(d[binsProp], v));
          }
          var closestSliceData;
          var closestSliceDistance = Infinity;
          var lastSliceData;
          hoverUpdate
            .attr("transform", function(d, i) {
              var index = hoverXIndex(d);
              lastSliceData = d;
              var x = xScale(d[binsProp][index][xProp] + d[binsProp][index][dxProp] / 2);
              var y = ySliceScale(d[binsProp][index][yProp]);
              var globalY = (mode === "offset" ? yScale(timeAccessor(d)) - (sliceHeight - y) : y);
              var dist = Math.abs(m[1] - globalY);
              if (dist < closestSliceDistance) {
                closestSliceDistance = dist;
                closestSliceData = d;
              }
              return "translate(" + x + "," + y + ")";
            });
          hoverUpdate.select("text").text(function(d) {
            var index = hoverXIndex(d);
            return d[binsProp][index][yProp];
          })
          hoverUpdate.classed("hover-closest", function(d) { return d === closestSliceData; });
          outlineUpdate.classed("outline-hover", function(d) { return d === closestSliceData; });

          var index = hoverXIndex(lastSliceData);

          xAxisHoverUpdate
              .attr("transform", function(d) {
                return "translate(" +
                  xScale(lastSliceData[binsProp][index][xProp] +
                         lastSliceData[binsProp][index][dxProp] / 2) + ", " +
                  height + ")";
              })
            .select("text")
              .text(function(d) { return format(lastSliceData[binsProp][index][xProp] +
                                                lastSliceData[binsProp][index][dxProp] / 2); });

          var fy = yAxis.tickFormat();
          yAxisHoverUpdate
              .attr("transform", function(d) {
                return "translate(" + width + ", " +
                  (mode === "offset" ? yScale(timeAccessor(closestSliceData)) : 0) + ")";
              })
              .style("display", mode === "offset" ? "" : "none")
            .select("text")
              .text(function(d) { return fy(timeAccessor(closestSliceData));});

          var fsy = ySliceAxis.tickFormat();
          ySliceAxisHoverUpdate
              .attr("transform", function(d) {
                return "translate(" + width + ", " +
                  (mode === "offset" ? 0 : ySliceScale(closestSliceData[binsProp][index][yProp])) +
                  ")";
              })
              .style("display", mode === "offset" ? "none" : "")
            .select("text")
              .text(function(d) { return fsy(closestSliceData[binsProp][index][yProp]); });

          var svgMouse = d3.mouse(svgNode);
          tooltip.style("transform", "translate(" + (svgMouse[0] + 15) + "px," +
              (svgMouse[1] - 15) + "px)")
            .select('span')
            .text(mode === "offset" ?
                fsy(closestSliceData[binsProp][index][yProp]) :
                (timeProp === "step" ? "step " : "") +
                fy(timeAccessor(closestSliceData)));
        }
      }
    });
    </script>

  </dom-module>
